using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Reflection;
using System.Text;

using Kenly.DBFramework.Gateway;

namespace Kenly.DBFramework.Reflection
{
    internal static class AssociationReflection
    {

        #region Common

        private static AssociationAttribute GetAssociationAttribute(PropertyInfo propertyInfo)
        {
            OneToOneAssociationAttribute oneToOneAssociationAttribute = null;
            OneToManyAssociationAttribute oneToManyAssociationAttribute = null;
            ManyToOneAssociationAttribute manyToOneAssociationAttribute = null;
            ManyToManyAssociationAttribute manyToManyAssociationAttribute = null;
            AssociationAttribute relationAttribute = AttributeUtility.GetAssociationAttribute(propertyInfo);
            if (relationAttribute == null)
            {
                oneToOneAssociationAttribute = AttributeUtility.GetCustomAttribute<OneToOneAssociationAttribute>(propertyInfo);
                if (oneToOneAssociationAttribute != null)
                {
                    return oneToOneAssociationAttribute;
                }
            }
            if (oneToOneAssociationAttribute == null)
            {
                oneToManyAssociationAttribute = AttributeUtility.GetCustomAttribute<OneToManyAssociationAttribute>(propertyInfo);
                if (oneToManyAssociationAttribute != null)
                {
                    return oneToManyAssociationAttribute;
                }
            }
            if (oneToManyAssociationAttribute == null)
            {
                manyToOneAssociationAttribute = AttributeUtility.GetCustomAttribute<ManyToOneAssociationAttribute>(propertyInfo);
                if (manyToOneAssociationAttribute != null)
                {
                    return manyToOneAssociationAttribute;
                }
            }
            if (manyToOneAssociationAttribute == null)
            {
                manyToManyAssociationAttribute = AttributeUtility.GetCustomAttribute<ManyToManyAssociationAttribute>(propertyInfo);
                if (manyToManyAssociationAttribute != null)
                {
                    return manyToManyAssociationAttribute;
                }
            }
            return relationAttribute;
        }

        private static PropertyInfo[] GetAssociationProperties(object entity)
        {
            if (entity == null || entity.GetType() == null)
            {
                return null;
            }

            List<PropertyInfo> propertyInfos = new List<PropertyInfo>();
            foreach (PropertyInfo propertyInfo in entity.GetType().GetProperties())
            {
                AssociationAttribute attribute = GetAssociationAttribute(propertyInfo);
                if (attribute != null)
                {
                    propertyInfos.Add(propertyInfo);
                }
            }
            return propertyInfos.ToArray();
        }


        /// <summary>
        /// Get the element type of a property if the property is an array or a generic type.
        /// </summary>
        /// <param name="propertyInfo"></param>
        /// <returns></returns>
        private static Type GetElementType(PropertyInfo propertyInfo)
        {
            if (propertyInfo.PropertyType.IsArray)
            {
                //the property is an array of element type.
                return propertyInfo.PropertyType.GetElementType();
            }
            else if (propertyInfo.PropertyType == typeof(DataTable))
            {
                AssociationAttribute relationAttribute = GetAssociationAttribute(propertyInfo);
                if (relationAttribute != null)
                {
                    return relationAttribute.ElementType;
                }
            }

            return propertyInfo.PropertyType;
        }

        private static object CreateElementInstance(PropertyInfo propertyInfo)
        {
            Type elementType = GetElementType(propertyInfo);
            object instance = System.Activator.CreateInstance(elementType);
            return instance;
        }

        #endregion

        #region Invoke Find Methods

        /// <summary>
        /// 
        /// </summary>
        /// <param name="parentEntity"></param>
        public static ArrayList InvokeFindMethods(object parentEntity)
        {
            ArrayList results = new ArrayList();
            PropertyInfo[] relationProperties = GetAssociationProperties(parentEntity);
            foreach (PropertyInfo propertyInfo in relationProperties)
            {
                object obj = InvokeFindMethodForProperty(parentEntity, propertyInfo);
                if (obj != null)
                {
                    results.Add(obj);
                }
            }
            return results;
        }

        public static ArrayList InvokeFindMethods(object parentEntity, params Type[] elementTypes)
        {
            ArrayList results = new ArrayList();
            if (elementTypes.Length <= 0)
            {
                return results;
            }
            PropertyInfo[] relationProperties = GetAssociationProperties(parentEntity);
            foreach (PropertyInfo propertyInfo in relationProperties)
            {
                foreach (Type elementType in elementTypes)
                {
                    if (GetElementType(propertyInfo) == elementType)
                    {
                        results.AddRange(InvokeFindMethods(parentEntity, elementType));
                    }
                }
            }
            return results;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="parentEntity"></param>
        /// <param name="elementType"></param>
        public static ArrayList InvokeFindMethods(object parentEntity, Type elementType)
        {
            ArrayList results = new ArrayList();
            PropertyInfo[] relationProperties = GetAssociationProperties(parentEntity);
            foreach (PropertyInfo propertyInfo in relationProperties)
            {
                if (GetElementType(propertyInfo) == elementType)
                {
                    object result = InvokeFindMethodForProperty(parentEntity, propertyInfo);
                    results.Add(result);
                }
            }
            return results;
        }


        /// <summary>
        /// Invoke the specified method.
        /// </summary>
        /// <param name="propertyInfo"></param>
        /// <param name="parentEntity"></param>
        private static object InvokeFindMethodForProperty(object parentEntity, PropertyInfo propertyInfo)
        {
            AssociationAttribute attribute = GetAssociationAttribute(propertyInfo);
            if (attribute == null)
            {
                return null;
            }
            //if (attribute.AssociationType == AssociationType.OneToOne || attribute.AssociationType == AssociationType.ManyToOne)
            //{
            //    if (propertyInfo.PropertyType.IsArray || propertyInfo.PropertyType == typeof(DataTable))
            //    {
            //        throw new GatewayException(MsgResource.PropertyTypeMustMatchAssociationType, propertyInfo);
            //    }
            //}

            //if (attribute.AssociationType == AssociationType.OneToMany || attribute.AssociationType == AssociationType.ManyToMany)
            //{
            //    if (!propertyInfo.PropertyType.IsArray && propertyInfo.PropertyType != typeof(DataTable))
            //    {
            //        throw new GatewayException(MsgResource.PropertyTypeMustMatchAssociationType, propertyInfo);
            //    }
            //}

            //if the property is writable,save the result to the property.
            if (!propertyInfo.CanWrite)
            {
                return null;
            }

            object[] parameters = GetMethodParameters(propertyInfo, parentEntity);
            Order order = GetOrder(propertyInfo);

            //return a entity,a datatable or an array of etitties.
            object result = InvokeFindMethodForProperty(propertyInfo, parameters, order);
            propertyInfo.SetValue(parentEntity, result, null);

            return result;

        }



        /// <summary>
        /// Invoke a method of the element type of the propery and return a EntityCollection maked from a DataTable.
        /// </summary>
        /// <param name="propertyInfo"></param>
        /// <param name="parameters"></param>
        /// <param name="order"></param>
        /// <returns></returns>
        private static object InvokeFindMethodForProperty(PropertyInfo propertyInfo, object[] parameters, Order order)
        {
            MethodInfo methodInfo = GetFindMethod(propertyInfo);
            object instance = CreateElementInstance(propertyInfo);
            SetOrder(instance, order);
            //return a EntityCollection
            object methodReturn = methodInfo.Invoke(instance, parameters);
            return methodReturn;
        }

        private static void SetOrder(object instance, Order order)
        {
            if (instance == null)
            {
                return;
            }

            Type gatewayBaseType = GetElementGatewayBaseType(instance.GetType());
            foreach (PropertyInfo propertyInfo in gatewayBaseType.GetProperties(BindingFlags.NonPublic | 
                BindingFlags.Static | BindingFlags.DeclaredOnly))
            {
                if (!propertyInfo.CanWrite)
                {
                    continue;
                }
                if (propertyInfo.PropertyType == typeof(Order) && propertyInfo.Name == "SortedOrder")
                {
                    propertyInfo.SetValue(instance, order, null);
                    return;
                }
            }
        }

        #endregion



        #region Get methods and coresponding parameters


        /// <summary>
        /// 
        /// </summary>
        /// <param name="propertyInfo"></param>
        /// <param name="container"></param>
        /// <returns></returns>
        private static object[] GetMethodParameters(PropertyInfo propertyInfo, object container)
        {
            Type elementType = GetElementType(propertyInfo);
            AssociationAttribute attribute = GetAssociationAttribute(propertyInfo);
            object[] parameters = new object[0];

            if (attribute.AssociationType == AssociationType.OneToOne)
            {
                parameters = GetOneToOneMethodParameters(elementType, container);
            }
            else if (attribute.AssociationType == AssociationType.OneToMany)
            {
                parameters = GetOneToManyMethodParameters(elementType, container, attribute);
            }
            else if (attribute.AssociationType == AssociationType.ManyToOne)
            {
                parameters = GetManyToOneMethodParameters(elementType, container);
            }
            else if (attribute.AssociationType == AssociationType.ManyToMany)
            {
                parameters = GetManyToManyMethodParameters(container, attribute);
            }

            return parameters;
        }


        private static object[] GetOneToOneMethodParameters(Type elementType, object container)
        {
            string containerPrimaryKey = KeyUtility.GetPrimaryKeyName(container.GetType());
            string elementPrimaryKey = KeyUtility.GetPrimaryKeyName(elementType);
            if (string.IsNullOrEmpty(containerPrimaryKey) && string.IsNullOrEmpty(elementPrimaryKey))
            {
                throw new GatewayException(MsgResource.ParentEntityMustContainPrimaryKey);
            }
            string containerForeignKey = KeyUtility.GetForeignKey(container.GetType(), elementType);
            string elementForeignKey = KeyUtility.GetForeignKey(elementType, container.GetType());
            if (string.IsNullOrEmpty(containerForeignKey) && string.IsNullOrEmpty(elementForeignKey))
            {
                throw new GatewayException(MsgResource.RelationAttributeShouldHasAForeignKey);
            }

            object keyValue = null;
            string condition = null;
            if (!string.IsNullOrEmpty(containerForeignKey))
            {
                keyValue = KeyUtility.GetKeyValue(container, containerForeignKey);
                condition = Expression.Equal(elementPrimaryKey, keyValue).ToString();
            }
            else
            {
                keyValue = KeyUtility.GetKeyValue(container, containerPrimaryKey);
                condition = Expression.Equal(containerPrimaryKey, keyValue).ToString();
            }
            object[] parameters = new object[1];
            parameters[0] = condition;
            return parameters;
        }

        private static object[] GetOneToManyMethodParameters(Type elementType, object container, AssociationAttribute attribute)
        {
            //the container should has a primary key that is a foreign key of relation object,
            //then it is possible to get the primary key value to create a search condition.
            string containerPrimaryKey = KeyUtility.GetPrimaryKeyName(container.GetType());
            if (string.IsNullOrEmpty(containerPrimaryKey))
            {
                throw new GatewayException(MsgResource.ParentEntityMustContainPrimaryKey);
            }
            string elementForeignKey = KeyUtility.GetForeignKey(elementType, container.GetType());
            if (string.IsNullOrEmpty(elementForeignKey))
            {
                elementForeignKey = containerPrimaryKey;
            }

            object containerPrimaryKeyValue = KeyUtility.GetKeyValue(container, containerPrimaryKey);
            string condition = Expression.Equal(elementForeignKey, containerPrimaryKeyValue).ToString();
            if (!string.IsNullOrEmpty(attribute.ResultFilter))
            {
                condition = condition + " AND " + attribute.ResultFilter;
            }
            object[] parameters = new object[1];
            parameters[0] = condition;
            return parameters;
        }


        private static object[] GetManyToOneMethodParameters(Type elementType, object container)
        {
            //the container should has a foreign key that is a primary key of relation object,
            //then it is possible to get the foreign key value to create a search condition.
            string elementPrimaryKey = KeyUtility.GetPrimaryKeyName(elementType);
            string containerForeignKey = KeyUtility.GetForeignKey(container.GetType(), elementType);
            if (string.IsNullOrEmpty(elementPrimaryKey))
            {
                throw new GatewayException(MsgResource.EntityMustContainPrimaryKey, elementType);
            }
            if (string.IsNullOrEmpty(containerForeignKey))
            {
                containerForeignKey = elementPrimaryKey;
            }
            object containerForeignKeyValue = KeyUtility.GetKeyValue(container, containerForeignKey);
            string condition = Expression.Equal(elementPrimaryKey, containerForeignKeyValue).ToString();
            object[] parameters = new object[1];
            parameters[0] = condition;
            return parameters;
        }

        private static object[] GetManyToManyMethodParameters(object container, AssociationAttribute attribute)
        {
            //the primary key of container
            string containerPrimaryKey = KeyUtility.GetPrimaryKeyName(container.GetType());
            if (string.IsNullOrEmpty(containerPrimaryKey))
            {
                throw new GatewayException(MsgResource.ParentEntityMustContainPrimaryKey, container);
            }
            //the foreignKey of interrelation type
            string interrelationForeignKey = KeyUtility.GetForeignKey(attribute.InterrelationType, container.GetType());
            if (string.IsNullOrEmpty(interrelationForeignKey))
            {
                interrelationForeignKey = containerPrimaryKey;
            }
            object containerPrimaryKeyValue = KeyUtility.GetKeyValue(container, containerPrimaryKey);

            string condition = Expression.Equal(interrelationForeignKey, containerPrimaryKeyValue);
            if (!string.IsNullOrEmpty(attribute.InterrelationFilter))
            {
                condition = condition + " AND " + attribute.InterrelationFilter;
            }
            object[] parameters = new object[2];
            parameters[0] = attribute.InterrelationType;
            parameters[1] = condition;
            return parameters;
        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="propertyInfo"></param>
        /// <returns></returns>
        private static MethodInfo GetFindMethod(PropertyInfo propertyInfo)
        {
            if (propertyInfo == null)
            {
                return null;
            }
            AssociationAttribute relationAttribute = GetAssociationAttribute(propertyInfo);
            Type elementGatewayBaseType = GetElementGatewayBaseType(propertyInfo);

            BindingFlags bindingFlags = BindingFlags.NonPublic | 
                BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;

            if (relationAttribute.AssociationType == AssociationType.OneToOne || 
                relationAttribute.AssociationType == AssociationType.ManyToOne)
            {
                return elementGatewayBaseType.GetMethod("FindTopOneForLateBound", bindingFlags);
            }

            if (relationAttribute.AssociationType == AssociationType.OneToMany)
            {
                if (propertyInfo.PropertyType == typeof(DataTable))
                {
                    return elementGatewayBaseType.GetMethod("QueryManyForLateBound", bindingFlags);
                }
                if (propertyInfo.PropertyType.IsArray)
                {
                    return elementGatewayBaseType.GetMethod("FindManyForLateBound", bindingFlags);
                }
            }

            if (relationAttribute.AssociationType == AssociationType.ManyToMany)
            {
                if (propertyInfo.PropertyType == typeof(DataTable))
                {
                    return elementGatewayBaseType.GetMethod("QueryManyByInterrelationForLateBound", bindingFlags);
                }
                if (propertyInfo.PropertyType.IsArray)
                {
                    return elementGatewayBaseType.GetMethod("FindManyByInterrelationForLateBound", bindingFlags);
                }
            }

            return null;
        }

        private static Order GetOrder(PropertyInfo propertyInfo)
        {
            AssociationAttribute attribute = GetAssociationAttribute(propertyInfo);
            if (attribute != null && !string.IsNullOrEmpty(attribute.ResultOrder))
            {
                Order order = new Order();
                order.OrderString = attribute.ResultOrder;
                return order;
            }
            return new Order();
        }


        private static Type GetElementGatewayBaseType(PropertyInfo propertyInfo)
        {
            Type elementGatewayBaseType = GetGatewayBaseType(GetElementType(propertyInfo));
            return elementGatewayBaseType;
        }

        private static Type GetElementGatewayBaseType(Type type)
        {
            Type elementGatewayBaseType = GetGatewayBaseType(type);
            return elementGatewayBaseType;
        }

        private static Type GetGatewayBaseType(Type baseType)
        {
            if (baseType.BaseType == typeof(object))
            {
                return baseType;
            }
            else
            {
                return GetGatewayBaseType(baseType.BaseType);
            }
        }

        #endregion


    }
}
